package server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.*;

public class Server implements Runnable {
    
    private ClientHandler lastClient = null;
    private ServerSocket serverSocket = null;
    private Deque<String> messages = null;
    private List<ClientHandler> clients = null;
    private PrintWriter clientOut = null;
    private BufferedReader clientIn = null;
    private InetAddress inetAdd;
    private int port = 4444;
    private int lowerBound = 1024;
    private int upperBound = 65535;
    
    /**
     * Get the port number the server is currently bound to (running on).
     * @return Server port number (integer).
     */
    public int getPort() {
        return port;
    }
    
    /**
     * Get a list of all the clients currently connected to the server instance.
     * @return A list of clients.
     */
    public List<ClientHandler> getClients() {
        return clients;
    }
    
    /**
     * Get a specific client by its position in the list.
     * @param i Index 
     * @return A client (ClientHandler object).
     */
    public ClientHandler getClient(int i) {
        if(i >= 0 && i < clients.size()) {
            return clients.get(i);
        }
        return null;
    }
    
    /**
     * Constructor: Creates a new Server object with a given port number.
     * @param _port A port between 1024 - 65535
     */
    public Server(int _port, InetAddress _inetAdd) {
        setPort(_port);
        inetAdd = _inetAdd;
    }
    
    /**
     * Add a message to be processed which the GUI server will handle.
     * @param msg A message (string)
     */
    public boolean addMessage(String msg) {
        if(messages == null) {
            return false;
        }
        return messages.add(msg);
    }
    
    /**
     * Retrieve a message to be processed/handled by the GUI server.
     * @return A message (string).
     */
    public String getMessage() {
        if(messages == null)
            return null;
        return messages.poll();
    }
    
    /**
     * Set the port number.
     * If the port number specified is lower or greater than the bounds/limits,
     * then a valid random port number will be specified instead.
     * @param _port Any integer between 1024 - 65535. 
     */
    private void setPort(int _port) {
        if(_port >= lowerBound && _port <= upperBound) {
            port = _port;
        }
        else {
            port = randomPort();
        }
    }
    
    /**
     * Generate a valid random port number.
     * @return A port number (integer).
     */
    private int randomPort() {
        return (new Random().nextInt(upperBound) + 1);
    }
    
    /**
     * Start the server.
     * @return Returns TRUE if the server was instantiated/started correctly.
     */
    public boolean start() {
        try {
            messages = new ArrayDeque<>();
            clients = new ArrayList<>();
            setPort(port);
            serverSocket = new ServerSocket(port, 10, inetAdd);
            
            addMessage("Server started on " + serverSocket.getLocalSocketAddress());
            addMessage("Listening for connections");
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * Stop the server.
     * Sends a message to all its clients that it has been stopped and 'kills'
     * (disconnects) all it's clients before closing the server socket.
     * @return Returns TRUE if the server was stopped correctly.
     */
    public boolean stop() {
        try {
            for(ClientHandler client : clients) {
                sendMessageToClient(client, "Server has been terminated! Try reconnect...");
                client.kill();
            }
            if(serverSocket != null && !serverSocket.isClosed())
                serverSocket.close();
            return true;
        } catch (IOException ex) {
            return false;
        }
    }
    
    public ClientHandler getLastConnectedClient() {
        ClientHandler client = lastClient;
        lastClient = null;
        return client;
    }
    
    /**
     * Listen for client connections.
     * If a client is trying to connect, then add it to the list of clients and
     * start the client immediately.
     */
    private void listenForClientConnection() {
        try {
            //Listen for a connection
            ClientHandler client = new ClientHandler(this, serverSocket.accept());
            lastClient = client;
            //If a client is connecting, then add it to the list of clients.
            clients.add(client);
            new Thread(client).start();
        } catch (IOException ex) {
            
        }
    }
    
    /**
     * Run the server.
     * This method will be called/run automatically when a new instance of the server
     * has been created in a new Thread (other than it was created on).
     * Usage:
        Server server = new Server(port);
        serverThread = new Thread(server);
        serverThread.start(); //The run() method will be called here.
         
     * While the server it listens for clients which are trying to connect.
     */
    @Override
    public void run() {
        start(); //start the server
        while(serverSocket != null) {
            listenForClientConnection(); //listen for clients
        }
    }

    /**
     * Loops through the list of clients and sends them messages.
     * @param message 
     */
    public void broadcastDataString(String message) {
        for(ClientHandler client : clients) {
            //Send message to the other clients
            //message = "[" + client.getName() + "]: " + message;
            sendMessageToClient(client, message);
        }
    }
    
    /**
     * Send message to a client if it is active (not disconnected).
     * @param client A client (ClientHandler object).
     * @param message A message (String).
     */
    public void sendMessageToClient(ClientHandler client, String message) {
        if(client.isActive()) {
            client.sendData(message);
        }
    }
}
